Android Media 实战二:音视频的录制(二)

Posted by alonealice on 2020-11-01

预览

在页面中添加SurfaceView,同时添加SurfaceHolder.Callback:

1
2
3
mSurfaceView = (SurfaceView) findViewById(R.id.surface_view);
mSurfaceHolder = mSurfaceView.getHolder();
mSurfaceHolder.addCallback(mSurfaceCallback);

在SurfaceHolder.Callback中的surfaceCreated方法中初始化相机,并开始预览:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mSurfaceCallback = new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
boolean suc = true;
if (mCamera == null) {
suc = initCamera();
}
if (suc) {
startPreview();
}
}


@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override
public void surfaceDestroyed(SurfaceHolder holder) {

}
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private boolean initCamera() {
int num = Camera.getNumberOfCameras();
if (num <= 0) {
return false;
}
boolean open = true;
try {
mCamera = Camera.open(0);
mCamera.setPreviewCallback(mPreviewCallback);
mParameters = mCamera.getParameters();
mParameters.setRotation(90);
mParameters.setPreviewFormat(ImageFormat.NV21); // 设置NV21预览格式
List<Camera.Size> list = mCamera.getParameters().getSupportedPreviewSizes();
if (list != null && !list.isEmpty()) {
Camera.Size size = getClosestSupportedSize(list, 720, 1080);
mParameters.setPreviewSize(size.width, size.height);//预览带下
mCamera.setParameters(mParameters);
mCamera.setDisplayOrientation(90);//预览方向
return true;
}
} catch (Exception e) {

}
return false;
}

相机开启预览:

1
2
mCamera.setPreviewDisplay(mSurfaceHolder);
mCamera.startPreview();//开始预览

视频数据保存

在相机初始化中添加PreviewCallback,每次预览画面更新时会回调onPreviewFrame方法:

1
2
3
4
5
6
7
8
9
10
11
12
mPreviewCallback = (bytes, camera) -> {
if (mIsRecording) {
Frame frame = new Frame();
frame.mData = bytes;
frame.mTime = System.nanoTime() / 1000;
if (frame.mTime - mPreviewImgTime > 1000 * 1000) {
mPreviewImgTime = frame.mTime;
}
//将预览的nv21数据传递给编码器
mVideoEncoder.addFrame(bytes);
}
};
1
2
3
4
5
6
7
private void startRecord() {
mMuxer = new MMuxer(getSaveVideoPath());
mVideoEncoder = new VideoEncoder2(mMuxer);
mVideoEncoder.prepare();
mVideoEncoder.start();
mIsRecording = true;
}

将onPreviewFrame回调的data,传到VideoEncoder进行编码保存。

VideoEncoder

VideoEncoder的prepare方法会初始化MediaCodecInfo和MediaFormat,同时设置相应的帧率码率,并创建MediaCodec。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public void prepare() {
MediaCodecInfo codecInfo = selectCodec(MIME_TYPE);
if (codecInfo == null) {
return;
}
mColorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar;//NV12数据格式
checkColorFormat(codecInfo, MIME_TYPE);
Log.i(TAG, "colorformat=" + mColorFormat);
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, 720, 1080);
mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat);
try {
mMediaCodec = MediaCodec.createByCodecName(codecInfo.getName());
} catch (Exception e) {
Log.i(TAG, e + "");
}
try {
if (!mIsAllKeyFrame) {
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //单位是 秒
} else {
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 0);//设置为0
}
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Log.i(TAG, "success configure-----------");
} catch (Exception e) {
Log.v(TAG, "config failed " + e);
try {
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); //单位是 秒
mMediaCodec.configure(mediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
Log.i(TAG, "config second success");
} catch (Exception e1) {
Log.i(TAG, "config second failed " + e1);
}
}

try {
mMediaCodec.start();
} catch (Exception e) {
Log.i(TAG, "start error--" + e);
}
mTrackIndex = -1;
}

videoEncoder实现runnable接口。在start方法中会启动线程:

1
2
3
4
public void start() {
mIsRecording = true;
new Thread(this).start();
}

在run方法中,会从帧队列中取出一帧输入,同时会间隔一段时间进行循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void run() {
int count = 0;
//轮询线程
while (true) {
count++;
if (mFrameList.size() > 0) {
Frame frame = mFrameList.remove(0);
input(frame);
}
try {
Thread.sleep(mLoopInterval);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (!mIsRecording) {
break;
}
}
}

addFrame主要是想队列中添加帧数据

1
2
3
4
5
6
public void addFrame(byte[] data) {
Frame frame = new Frame();
frame.mTime = System.nanoTime() / 1000;
frame.mData = data;
mFrameList.add(frame);
}

input方法一个是将数据进行转化,同时向MediaCodec的inputBuffers添加数据,然后会调用output方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void input(Frame frame) {
ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers();
int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
byte[] dst = null;
if (frame.mData != null) {
dst = new byte[frame.mData.length];
//nv21转nv12
NV21toI420SemiPlanar(frame.mData, dst, 720, 1080);
}
if (inputBufferIndex >= 0) {
ByteBuffer inputBuffer = inputBuffers[inputBufferIndex];
inputBuffer.clear();
if (dst != null) {
inputBuffer.put(dst);
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, dst.length, frame.mTime, 0);
output(false);
} else {
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, frame.mTime, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
output(true);
}
}
}

output方法主要是向MMuxer添加轨道,同时从MediaCodec获取数据并写入到MMuxer,而MMuxer跟音视频的录制(一)中相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public void output(boolean isEos) {
String tag = TAG + "-output";
ByteBuffer[] outputBuffers = null;
int count = 0;
int outputIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
try {
outputBuffers = mMediaCodec.getOutputBuffers();
do {
if (outputIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
Log.i(tag, "output from encoder not available");
if (!isEos) {
count++;
if (count >= 10) {
// break;
}
}
} else if (outputIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = mMediaCodec.getOutputBuffers();
Log.i(tag, "encoder output buffers changed");
} else if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
//加入视频轨道编码模式
addTrack();
Log.i(tag, "encoder output format change");
} else if (outputIndex < 0) {
Log.e(tag, "output buffer wrong " + outputIndex);
} else {
ByteBuffer outputBuffer = outputBuffers[outputIndex];
if (outputBuffer == null) {
Log.e(tag, "output buffer null");
return;
}
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
mBufferInfo.size = 0;
}
Log.d(tag, "buffer size=" + mBufferInfo.size + " pts=" + mBufferInfo.presentationTimeUs);
if (mBufferInfo.size != 0) {
if (!mMuxer.isVideoTrackAdd()) {
addTrack();
}
if (!mMuxer.isStarted()) {
mMuxer.start();
}
outputBuffer.position(mBufferInfo.offset);
outputBuffer.limit(mBufferInfo.offset + mBufferInfo.size);
//编码后的数据输出到混合器
mMuxer.writeSampleData(mTrackIndex, outputBuffer, mBufferInfo);
}
mMediaCodec.releaseOutputBuffer(outputIndex, false);
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// when EOS come.
mIsRecording = false;
stopMuxer();
release();
break; // out of while
}
}
outputIndex = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);

} while (outputIndex >= 0);
} catch (Exception e) {
}

}